#include "world/map.hpp"

#include <stack>
#include <string>
#include <vector>

#include <ctime>
#include <cstdlib>

#include <fstream>
#include <iostream>

#include <json.hpp>

using namespace std;
using namespace dw;

inline
int getRand(int min, int max)
{
    if (min <= max) {
        return (rand() % (max - min + 1)) + min;
    } else {
        return (rand() % (min - max + 1)) + max;
    }
}

const string Map::block[] = {
    "\033[47m  \033[0m", // WHITE
    "\033[40m  \033[0m", // BLACK
    "\033[41m  \033[0m", // RED
    "\033[42m  \033[0m", // GREEN
    "\033[43m  \033[0m", // BROWN
    "\033[44m  \033[0m", // BLUE
    "\033[45m  \033[0m", // PINK
    "\033[46m  \033[0m", // CYAN
    "\033[49m  \033[0m"  // default background color
};

Map::Map(uint32_t _row, uint32_t _col, Map::Type _type, uint32_t _id)
{
    this->id = _id;
    this->row = _row;
    this->col = _col;
    this->buf = new uint8_t[row * col + 1]{0};

    generate(_type);
}

Map &Map::set(const Map::State &_state)
{
    this->id = _state.id;

    if (row * col < _state.row * _state.col)
    {
        if (this->buf){
            delete[] this->buf;
        }
        this->buf = new uint8_t[_state.row * _state.col + 1];
    }

    this->row = _state.row;
    this->col = _state.col;

    for (uint32_t i = 0; i < this->row; i++)
    {
        for (uint32_t j = 0; j < this->col; j++)
        {
            this->buf[i * this->col + j] = _state.map[i * this->col + j] - '0';
        }
    }

    buf[row * col] = 0;

    return *this;
}

bool Map::generate(Map::Type _type, float WhitePercent)
{
    switch (_type)
    {
        case Map::Type::PERLIN:
            return this->PerlinGenerate(WhitePercent);
        case Map::Type::MAZE:
            return this->MazeGenerate();
        default:
            return false;
    }
}

bool Map::PerlinGenerate(float WhitePercent)
{
	if (WhitePercent < 0 || WhitePercent >= 1)
	{
		cout << "White Percent must be between (0, 1)" << endl;
		return false;
	}

    if (WhitePercent == 0.0f) {
        WhitePercent = 0.6f;
    }

    vector<vector<float>> baseNoise = GenerateWhiteNoise(this->row, this->col);
	vector<vector<float>> perlinNoise = GeneratePerlinNoise(baseNoise, 3);

	for (int y = 0; y < int(this->row); ++y)
	{
		for (int x = 0; x < int(this->col); ++x)
		{
			if (perlinNoise[y][x] > WhitePercent)
				this->operator[]({x, y}) = 1;
			else
                (*this)[{x, y}] = 0;
        }
	}
	
	for (int k = 0; k < 2; k++)
	{
		for (int y = 0; y < int(this->row); y++)
		{
			for (int x = 0; x < int(this->col); x++)
			{
				int num = 0;

                // First Row
				if (y == 0 && x > 0 && x < int(this->col - 1)) {
                    num = (*this)[{x, y + 1}]        // . 0 1 2 3 4
                          + (*this)[{x - 1, y}]      // 0 HH xHH   
                          + (*this)[{x + 1, y}]      // 1 HHHHHH   
                          + (*this)[{x - 1, y + 1}]  // 2          
                          + (*this)[{x + 1, y + 1}]; // 3          
                }

                // Last Row
				if (y == this->row - 1 && x > 0 && x < int(this->col - 1)) {
					num = (*this)[{x, y - 1}]       // 0           
                        + (*this)[{x - 1, y}]       // 1           
                        + (*this)[{x + 1, y}]       // 2 HHHHHH    
                        + (*this)[{x + 1, y - 1}]   // 3 HH xHH    
                        + (*this)[{x - 1, y - 1}];  // . 1 2 3 4 5 
                }

				// First Col
                if (x == 0 && y > 0 && y < int(this->row - 1)) {
                    num = (*this)[{x, y - 1}]        // . 0 1 2 3 4
                          + (*this)[{x, y + 1}]      // 0 HHHH
                          + (*this)[{x + 1, y}]      // 1 x HH
                          + (*this)[{x + 1, y - 1}]  // 2 HHHH
                          + (*this)[{x + 1, y + 1}]; // 3
                }

				// Last Col
                if (x == this->col - 1 && y > 0 && y < int(this->row - 1)) {
                    num = (*this)[{x, y - 1}]        // . 0 1 2 3 4
                          + (*this)[{x, y + 1}]      // 0       HHHH
                          + (*this)[{x - 1, y}]      // 1       HH x
                          + (*this)[{x - 1, y - 1}]  // 2       HHHH
                          + (*this)[{x - 1, y + 1}]; // 3
                }
                
                // Center
                if (y > 0 && y < int(this->row - 1) && x > 0 && x < int(this->col - 1)) {
					num = (*this)[{x, y - 1}]       // . 0 1 2 3 4 5 6
                        + (*this)[{x, y + 1}]       // 0      
                        + (*this)[{x - 1, y}]       // 1      
                        + (*this)[{x + 1, y}]       // 2     HHHHHH 
                        + (*this)[{x + 1, y - 1}]   // 3     HH xHH       
                        + (*this)[{x - 1, y - 1}]   // 4     HHHHHH
                        + (*this)[{x - 1, y + 1}]   // 5   
                        + (*this)[{x + 1, y + 1}];  // 6   
				}
                
                if ((*this)[{x, y}] == 1 && num < 3)
					(*this)[{x, y}] = 0;
				else if ((*this)[{x, y}] == 0 && num > 5)
					(*this)[{x, y}] = 1;
			}
		}
	}

	return true;
}

bool Map::MazeGenerate()
{
    if (this->col < 4 || this->row < 4)
    {
		cout << "map is too small!";
		return false;
	}

	srand((uint32_t)time(0));
	Grid start(this->col / 2, this->row / 2);
    (*this)[start] = 1;

    stack<Grid> visited;
	visited.push(start);

	Grid point(start);
    while (1)
    {
        using Vec = Grid;

        vector<Map::Direction> put;
		if (point.y > ROUTHWIDTH)
		{
			int temp = 0;
			for (int i = 1; i <= ROUTHWIDTH + 1; i++)
			{
				temp += (*this)[point - Vec{0, i}];
			}
			if (temp == 0)
				put.push_back(Map::Direction::UP);
		}
		if (point.y < int(this->row - ROUTHWIDTH - 1))
		{
			int temp = 0;
			for (int i = 1; i <= ROUTHWIDTH + 1; i++)
			{
                temp += (*this)[point + Vec{0, i}];
            }
			if (temp == 0)
                put.push_back(Map::Direction::DOWN);
        }
		if (point.x > ROUTHWIDTH)
		{
			int temp = 0;
			for (int i = 1; i <= ROUTHWIDTH + 1; i++)
			{
                temp += (*this)[point - Vec{i, 0}];
            }
			if (temp == 0)
                put.push_back(Map::Direction::LEFT);
        }
		if (point.x < int(this->col - ROUTHWIDTH - 1))
		{
			int temp = 0;
			for (int i = 1; i <= ROUTHWIDTH + 1; i++)
			{
                temp += (*this)[point + Vec{i, 0}];
            }
			if (temp == 0)
                put.push_back(Map::Direction::RIGHT);
        }

        if (put.empty())
		{
			visited.pop();
            if (visited.empty()) return 0;
            point = visited.top();
        }
		else
		{
			// Randomly choose an accessable point
			int choose = rand() % put.size();
			switch (put[choose]) 
			{
            case Map::Direction::UP:
                point = Grid(point.x, point.y - ROUTHWIDTH - 1);
                for (int i = 0; i < ROUTHWIDTH + 1; i++)
                {
                    (*this)[point + Vec{0, i}] = 1;
                }
				break;
            case Map::Direction::DOWN:
                point = Grid(point.x, point.y + ROUTHWIDTH + 1);
                for (int i = 0; i < ROUTHWIDTH + 1; i++)
				{
                    (*this)[point - Vec{0, i}] = 1;
                }
				break;
            case Map::Direction::LEFT:
                point = Grid(point.x - ROUTHWIDTH - 1, point.y);
                for (int i = 0; i < ROUTHWIDTH + 1; i++)
				{
                    (*this)[point + Vec{i, 0}] = 1;
                }
				break;
            case Map::Direction::RIGHT:
                point = Grid(point.x + ROUTHWIDTH + 1, point.y);
                for (int i = 0; i < ROUTHWIDTH + 1; i++)
				{
                    (*this)[point - Vec{i, 0}] = 1;
                }
				break;
			default:
                break;
			}
			visited.push(point);
		}
	}

	return true;
}

bool Map::save(const string& mapPath)
{
	ofstream savefile(mapPath);
	if (!savefile.is_open())
	{
        cerr << "Map::save(): Can't open file: \"" << mapPath << "\"" << endl;
        return false;
    }

    Map::State m_state = this->getMapState();
    savefile << m_state.id  << endl;
    savefile << m_state.row << endl;
    savefile << m_state.col << endl;
    savefile << m_state.map << endl;

    savefile.close();
    return true;
}

bool Map::load(const string& mapPath)
{
	ifstream loadfile(mapPath);
	if (!loadfile.is_open())
	{
        cerr << "Map::load(): Can't open file: \"" << mapPath << "\"" << endl;
        return false;
    }

    string s;
	int line = 0;
    Map::State m_state;
    while (getline(loadfile, s))
    {
		stringstream ss;
		ss << s;
		if (!ss.eof())
		{
			switch (line)
			{
			    case 0: ss >> m_state.id; break;
			    case 1: ss >> m_state.row; break;
			    case 2: ss >> m_state.col; break;
			    default:ss >> m_state.map; break;
			}
		}
		line++;
	}

	loadfile.close();

    this->set(m_state);
    return true;
}

string Map::encode() const
{
    return this->getMapState().encode();
}

Map& Map::decode(const string& data_msg)
{
    Map::State m_state;
    m_state.decode(data_msg);
    this->set(m_state);

    return *this;
}

std::string Map::compress() const {
    return this->getMapState().compress();
}

Map& Map::decompress(const std::string& str) {
    Map::State m_state;
    m_state.decompress(str);
    this->set(m_state);

    return *this;
}

Map::State Map::getMapState() const
{
    Map::State m_state;
    m_state.id  = this->id;
    m_state.row = this->row;
    m_state.col = this->col;

    // Construct Map::State::map from Map::buf
    for (int y = 0; y < int(this->row); y++)
    {
        for (int x = 0; x < int(this->col); x++)
        {
            m_state.map += (*this)[{x, y}] + '0';
        }
    }
    m_state.map += '\0';

    return m_state;
}

Map Map::getSubMap(const Grid& LT, const Grid& RB, uint32_t _id) const
{ 
    // Boundary Check
    if (!isValidCoord(LT) || !isValidCoord(RB))
    {
        cerr << "Map::getSubMap(): Region out of range: get "
             << LT << " to " << RB << " from "
             << this->col << " x " << this->row << " Map" << endl;
        return Map();
    }

    Grid LeftTop;
    Grid RightBottom;
    // Make LeftTop really LeftTop
    if (LT.x > RB.x) {
        RightBottom.x = LT.x;
        LeftTop.x = RB.x;
    } else {
        RightBottom.x = RB.x;
        LeftTop.x = LT.x;
    }

    if (LT.y > RB.y) {
        RightBottom.y = LT.y;
        LeftTop.y = RB.y;
    } else {
        RightBottom.y = RB.y;
        LeftTop.y = LT.y;
    }

    Map::State submap_state;
	submap_state.id = _id;
    submap_state.row = RightBottom.y - LeftTop.y;
    submap_state.col = RightBottom.x - LeftTop.x;
	for (int y = LeftTop.y; y < int(submap_state.row); y++)
	{
		for (int x = LeftTop.x; x < int(submap_state.col); x++)
		{
			submap_state.map += this->buf[x + y * col] + '0';
		}
	}
    submap_state.map += '\0';

    return Map(submap_state);
}

Coord Map::getSpawnPoint()
{
    do
    {
        int x = getRand(0, this->col);
        int y = getRand(0, this->row);

        if (!this->isBlock({x, y}))
        {
            return Coord({x + 0.5, y + 0.5});
        }
    } while (1);
}

bool Map::show()
{
    cout << *this << endl;
    return true;
}

namespace dw
{
    void to_json(nlohmann::json &j, const Map &map)
    {
        to_json(j, map.getMapState());
    }

    void from_json(const nlohmann::json &j, Map &map)
    {
        Map::State state;
        from_json(j, state);
        map.set(state);
    }
    ostream &operator<<(ostream &os, const Map &map)
    {
        os << "Map ID: " << map.id << endl;
        os << "Map Size (r * c): " << map.row << " x " << map.col << endl;

        os << setw(3) << "";
        for (int x = 0; x < (int)min(map.col, uint32_t(80)); x++)
        {
            os << setw(2) << x;
        }
        os << endl;

        for (int y = 0; y < (int)min(map.row, uint32_t(80)); y++)
        {
            os << setw(2) << y << " ";
            for (int x = 0; x < (int)min(map.col, uint32_t(80)); x++)
            {
                // os << (map.isBlock({x, y}) ? Map::block[Map::Color::BLACK] : Map::block[Map::Color::WHITE]);
                os << Map::block[Map::Color(map[{x, y}])];
            }
            os << endl;
        }
        return os;
    }
}

/***********************************
 * 
 *      Map::State
 * 
 * *********************************/

string Map::State::compress() const
{
    string result;

    int count = 0;
    char first = this->map[0];
    for (size_t i = 0; i < this->map.length(); i++)
    {
        if (first == this->map[i])
        {
            count++;
        }
        else
        {
            result += this->map[i - 1];
            result += to_string(count);
            result += "s";
            count = 1;
            first = this->map[i];
        }
    }
    result += this->map[this->map.size() - 1];
    result += to_string(count);
    result += "s";

    return result;
}

Map::State& Map::State::decompress(const string &encoded)
{
    string str = encoded;
    string &result = this->map;
    if (str.empty()) return *this;

    // Split First
    vector<string> split_result;
    string pattern = "s";
    str += pattern;
    size_t pos = str.find(pattern);
    while (pos != 0)
    {
        string temp = str.substr(0, pos);
        split_result.push_back(temp);
        
        // Remove splited part
        str = str.substr(pos + 1, str.size());
        pos = str.find(pattern);
    }
    
    for (size_t i = 0; i < split_result.size(); i++)
    {
        int num = stoi(split_result[i].substr(1, split_result[i].size()));
        for (int j = 0; j < num; j++)
        {
            result += split_result[i][0];
        }
    }
    
    return *this;
}

string Map::State::encode() const
{
    string compressed_map;
    compressed_map = this->compress();

    using json = nlohmann::json;
    json root;
    root["map_state"] = {};

    json &jMapState = root["map_state"];

    jMapState["id"] = this->id;
    jMapState["row"] = this->row;
    jMapState["col"] = this->col;
    jMapState["map"] = compressed_map;

    return root.dump();
}

Map::State &Map::State::decode(const string& data_msg)
{
    using json = nlohmann::json;
    json root = json::parse(data_msg.begin(), data_msg.end());
    json& jMapState = root["map_state"];

    string decompressed_map;

    jMapState.at("id").get_to(this->id);
    jMapState.at("row").get_to(this->row);
    jMapState.at("col").get_to(this->col);
    jMapState.at("map").get_to(decompressed_map);

    this->decompress(decompressed_map);

    return *this;
}

Map::State Map::State::scale(int scalar)
{
    if (scalar == 1) return *this;
    
    Map::State scaled_map;
    scaled_map.id  = this->id;
    scaled_map.row = this->row * scalar;
    scaled_map.col = this->col * scalar;

    scaled_map.map.resize(scaled_map.row * scaled_map.col + 1, '0');

    for (int y = 0; y < (int)row; y++)
    {
        int Y = y * scalar;
        for (int x = 0; x < (int)col; x++)
        {
            int X = x * scalar;
            for (int dy = 0; dy < scalar; dy++)
            {
                for (int dx = 0; dx < scalar; dx++)
                {
                    scaled_map[{X + dx, Y + dy}] = (*this)[{x, y}];
                }
            }
        }
    }
    scaled_map.map[scaled_map.row * scaled_map.col] = '\0';

    return scaled_map;
}

Map::State Map::State::expand(int radius)
{
    Map::State extended_map;
    extended_map = *this;

    // Left
    for (int y = 0; y < (int)row; y++)
    {
        for (int x = 1; x < (int)col; x++)
        {
            if (this->isBlock({x, y}))
            {
                for (int k = 1; k <= radius; k++)
                {
                    if (x < k) break;
                    if (this->isBlock({x - k, y})) break;

                    extended_map[{x - k, y}] = '3';
                }
            }
        }
    }

    // Right
    for (int y = 0; y < (int)row; y++)
    {
        for (int x = 0; x < (int)col - 1; x++)
        {
            if (this->isBlock({x, y}))
            {
                for (int k = 1; k <= radius; k++)
                {
                    if (x + k >= (int)col) break;
                    if (this->isBlock({x + k, y})) break;

                    extended_map[{x + k, y}] = '3';
                }
            }
        }
    }

    // Up
    for (int y = 1; y < (int)row; y++)
    {
        for (int x = 0; x < (int)col; x++)
        {
            if (extended_map.isBlock({x, y}))
            {
                for (int k = 1; k <= radius; k++)
                {
                    if (y < k) break;
                    if (extended_map.isBlock({x, y - k})) break;

                    extended_map[{x, y - k}]= '3';
                }
            }
        }
    }

    // Down
    for (int x = 0; x < (int)col; x++)
    {
        for (int y = row - 2; y >= 0; y--)
        {
            if (extended_map.isBlock({x, y}))
            {
                for (int k = 1; k <= radius; k++)
                {
                    if (y + k >= (int)row) break;
                    if (extended_map.isBlock({x, y + k})) break;

                    extended_map[{x, y + k}]= '3';
                }
            }
        }
    }

    return extended_map;
}

Coord Map::State::getAccessableCoord() const {
    do
    {
        int x = getRand(0, this->col);
        int y = getRand(0, this->row);

        if (!this->isBlock({x, y}))
        {
            return Coord({x + 0.5, y + 0.5});
        }
    } while (1);
}

namespace dw
{
    void to_json(nlohmann::json &j, const Map::State &state)
    {
        std::string compressed_map = state.compress();

        j = nlohmann::json{
            {"id", state.id},
            {"row", state.row},
            {"col", state.col},
            {"map", compressed_map}};
    }

    void from_json(const nlohmann::json &j, Map::State &state)
    {
        std::string compressed_map;

        j.at("id").get_to(state.id);
        j.at("row").get_to(state.row);
        j.at("col").get_to(state.col);
        j.at("map").get_to(compressed_map);

        state.decompress(compressed_map);
    }

    ostream &operator<<(ostream &os, const Map::State &map_state)
    {
        os << "Map ID: " << map_state.id << endl;
        os << "Map Size (r * c): " << map_state.row << "x" << map_state.col << endl;
        os << "Map Content:" << endl;
        os << map_state.map;

        return os;
    }
}
